#!/usr/bin/env python
#!
#! docmaker - a simple tool to blend code and LaTeX
#! Copyright (C) 2012 Alex Voss
#!
#! This program is free software: you can redistribute it and/or modify
#! it under the terms of the GNU General Public License as published by
#! the Free Software Foundation, either version 3 of the License, or
#! (at your option) any later version.
#! 
#! This program is distributed in the hope that it will be useful,
#! but WITHOUT ANY WARRANTY; without even the implied warranty of
#! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#! GNU General Public License for more details.
#! 
#! You should have received a copy of the GNU General Public License
#! along with this program.  If not, see <http://www.gnu.org/licenses/>.
#!
#>\documentclass[a4paper]{article}
#>\usepackage[hmargin=2.5cm,vmargin=3cm]{geometry}
#>\usepackage{lastpage}
#>\begin{document}
#>\title{docmaker - a simple tool to blend code and LaTeX}
#>\author{Alex Voss}
#>\maketitle
#>\abstract{\verb!docmaker! is a simple command that allows a developer to interleave code
#>with documentation written in LaTeX and to extract a LaTeX document from this that can
#>be compiled to generate a readable presentation of the source code and its documentation.
#>The aim is to make this really easy by reducing the tool to a bare minimum and leveraging
#>the power of LaTeX and the ecosystem of tools that has developed around it. The complete
#>source code for \verb!docmaker! and its full documentation fit onto \pageref{LastPage}
#>pages. }
#>\section{About this program}
#>The purpose of this little program is to allow people to document their code in a way
#>that allows them to create a version formatted for reading using LaTeX. The reason I
#>wrote it is because I was getting fed up with existing tools that were too complex
#>to learn for someone with little time (or patience or both) and were forcing people to
#>use a particular documentation style. 
#>
#>These tools seemed to fall into two classes.
#>One consists of tools like DoxyGen that aim at producing documentation for the use
#>of people reusing someone elses code as a black box (library, component, whatever).
#>That is, they document mainly the interfaces involved in the code, the method signatures
#>or classes that facilitate reuse. The other class comprises tools inspired mainly by
#>Donald Knuth's Web system that aims to make the code itself readable and enjoyable.
#>Unfortunately, these tools come with a whole philosophy that may or may not fit the 
#>needs of a user and that is difficult to retrospectively apply to a piece of code.
#>
#>This inspired me to write \verb!docmaker!, a simple tool that allows a developer to interleave
#>code with documentation written in LaTeX to produce a human-readable, fully documented
#>listing with minimum effort. All you need to know is that the tool will read your code
#>line-by-line and determine if a line starts with a set of characters that initiate a
#>single line comment in the programming language you use, e.g., the hash symbol in Python
#>or Bash code, a sequence of two slashes in C or C++ or an exclamation mark in Fortran. 
#>If these characters are immediately followed by a \verb!>! sign then they will be removed from
#>the output. If the line does not start with a comment character or if it is not followed
#>by the \verb!>! symbol then the line is embedded in a \verb!\verbatim{...}! environment so
#>that LaTeX outputs it as formatted and in a monospace font. That is pretty much it. Have a 
#>look at the code of this tool itself to see how it works, it's really quite simple.
#>
#>\verb!docmaker! is published under the GNU General Public License Version 3, see its source
#>code for details. Please contact me at \verb!alex.voss@st-andrews.ac.uk! if you have any
#>questions or comments on this program.
#>
#>\section{Tipps}
#>
#>Here are some tipps for using \verb!docmaker!:
#>\begin{itemize}
#>\item To avoid having lengthy sections of written text in your code you can use the \verb!\input{...}! LaTeX command.
#>\item Interleaving the code with lengthy documentation -- as is encouraged -- means that you may lose sight of the gross structure of the code, especially
#>when using a language like Python that relies on indentation. You can overcome this to an extent by using numbered sections.
#>\end{itemize}
#>
#>\section{Imports}
#>The only imports required here are \verb!os!, \verb!io! and \verb!argparse!. Quite minimalistic, really, and 
#>should work on any system where Python 2.7 upwards is installed.
import os
import io
import argparse
#>\section{Class DocMaker}
#>This class contains all the functionality required. I program a lot in Java, so please forgive me for 
#>creating a class here where I am sure a plain old script would have sufficed.
class DocMaker():
  #>\subsection{Instance Variables}
  #>List of supported languages with their respective settings. The \verb!extensions! element is used to
  #>guess the language in the \verb!guessLanguage! method, the \verb!commentStart! element is used to
  #>identify the start of a comment and the \verb!commentIgnore! one to identify comments that should be
  #>silently ignored.
  languages = {
      'python': { 'extensions': ['.py'], 'commentStart': '#', 'commentIgnore': '#!' },
      'fortran': { 'extensions': ['.f90'], 'commentStart': '!'},
      'java': { 'extensions': ['.java'], 'commentStart': '//'},
      'idl': { 'extensions': ['.pro'], 'commentStart': ';'}, 
  }
  #>arguments passed to the program and parsed in the \verb!parseArgs! method.
  args = None
  #>\subsection{makeDoc}
  #>Parses the command line arguments and produces the documented source code for each of the input files specified.
  def makeDoc(self):
    self.parseArgs()
    for inputFile in self.args.sourcefiles:
      settings = self.guessLanguage(inputFile)
      #>create the output filename
      if self.args.output is None:
        (root, ext) = os.path.splitext(inputFile)
        outputFile = root+".tex"
      else:
        outputFile = self.args.output
      self.generateDoc(inputFile, outputFile, settings)
  #>\subsection{parseArgs}
  #>At the moment the only arguments are the input files to process. Run with \verb!--help! to see how this is used.
  #>Later on we may add additional features such as specifying the input lanaguage but guessing works fine at the moment.
  def parseArgs(self):
    parser = argparse.ArgumentParser(description="Produce documented version of code")
    parser.add_argument("--output", nargs="?", help="specify an output file")
    parser.add_argument("sourcefiles", metavar="file", nargs="+", help="source files to process")
    self.args = parser.parse_args()
  #>\subsection{guessLanguage}
  #>Guesses the source code language on the basis of the filename extension using the
  #>\verb!languages! instance variable.
  def guessLanguage(self, inputFile):
    (root, ext) = os.path.splitext(inputFile)
    for lang,settings in self.languages.items():
      if ext in settings['extensions']:
        return settings
  #>\subsection{generateDoc}
  #>produces documentation for a given input file
  def generateDoc(self, inputFile, outputFile, settings):
    #>flag to keep track of whether a \verb!verbatim! environment is open in the output or not
    verbatim = False
    #>the character combination that signals the start of a comment in the given language
    commentStart = settings["commentStart"]
    #>\verb!commentIgnore! is set if there is a type of comment to ignore in the language
    commentIgnore = None
    if settings.has_key("commentIgnore"):
      commentIgnore = settings["commentIgnore"]
    #>read open input and output file
    with io.open(inputFile, mode="rt", encoding="utf8") as f:
      with io.open(outputFile, mode="wt", encoding="utf8") as o:
        #>and read the input line by line
        for line in f:
          #>if the line is a docmaker comment
          if line.lstrip().startswith(commentStart+'>'):
            #>close any open verbatim environment
            if verbatim:
              o.write(u"\\"+u"end{verbatim}}\n\n\\noindent\n")
              verbatim = False
            #>write out the line minus the comment character(s) and \verb!>!
            o.write(line.lstrip()[len(commentStart)+1:])
          #>else (line is not a docmaker comment)
          else:
            #>process it only if it not a comment that needs to be ignored (like \verb=#!=)
            if commentIgnore is None or not line.lstrip().startswith(commentIgnore):
              #>if a verbatim environment is not open already then open one
              if not verbatim:
                o.write(u"{\\"+u"footnotesize\\"+u"begin{verbatim}\n")
                verbatim = True
              #>write out the line of code
              o.write(line)
        #>at the end of the for loop close any open verbatim environment
        if verbatim:
          o.write(u"\\"+u"end{verbatim}}\n\n\\noindent\n")
#>\section{Main Program}
#>instantiate the \verb!DocMaker! and invoke the \verb!makeDoc! method
md = DocMaker()
md.makeDoc()
#>\end{document}
